<?php

class CliContext
{
	private $parent;
	private $variables;
	private $settings;
	private $userfuncs;
	private $mode;
	private $errors;
	private $parser;

	public function __construct (CliMode $mode)
	{
		$this->mode = $mode;
		$this->parent = false;
		$this->variables = array();
		$this->settings = array();
		$this->userfuncs = array();
		$this->errors = array();
		$this->parser = null;
	}

	public function createChild ()
	{
		$context = new CliContext($this->mode);
		$context->parent = $this;
		return $context;
	}

	public function getMode ()
	{
		return $this->mode;
	}

	public function setVariable ($index, array $arrayref, $value)
	{
		// Make sure the identifier is not reserved
		if ($this->mode->getIdentifierType($index) !== false ||
			isset($this->userfuncs[$index]))
		{
			throw new CliReservedException(
				"Identifier '$index' is as reserved name");
		}

		$ref = & $this->variables;

		do
		{
			// If the array isn't an array, make it so
			if (!is_array($ref))
			{
				$ref = array();
			}

			// If the index doesn't exist yet, format it
			if (!isset($ref[$index]))
			{
				$ref[$index] = false;
			}

			$ref = & $ref[$index];
		}
		while (($index = array_pop($arrayref)) !== null);

		$ref = $value;
	}

	public function getVariable ($name, array $arrayref)
	{
		$ref = $this->resolveReference($name, $arrayref, 'variables');

		if (is_array($ref))
		{
			throw new CliReferenceException(
				"Cannot convert from array to value");
		}

		return $ref;
	}

	public function getSize ($name, array $arrayref)
	{
		$ref = $this->resolveReference($name, $arrayref, 'variables');

		if (!is_array($ref))
		{
			throw new CliReferenceException(
				"Can not return size of non array variable");
		}

		return count($ref);
	}

	public function setSetting ($name, $value)
	{
		$setting = $this->mode->getSetting($name);

		if ($setting['validator'] === false)
		{
			$value = $this->validateType($value, gettype($setting['default']));
		}
		else
		{
			$value = call_user_func($setting['validator'], $value);
		}

		$this->settings[$name] = $value;
	}

	private function validateType ($value, $deftype)
	{
		$newtype = gettype($value);

		// New and default value must have the same type
		if ($deftype != $newtype)
		{
			// Perform proper conversion if there is a type mismatch
			switch ($deftype)
			{
				case 'boolean':
					if ($newtype == 'double' || $newtype == 'double')
					{
						$value = round($value) != 0;
					}
					elseif ($newtype == 'string')
					{
						if (in_array($value, array('true', 'yes', 'on',
							'enable', 'enabled', '1')))
						{
							$value = true;
						}
						elseif (in_array($value, array('false', 'no', 'off',
							'disable', 'disabled', '0')))
						{
							$value = false;
						}
					}

					break;

				case 'integer':
				case 'double':
					if ($newtype == 'integer' || $newtype == 'double' ||
						($newtype == 'string' && is_numeric($value)))
					{
						settype($value, $deftype);
					}
					elseif ($newtype == 'boolean')
					{
						$value = $value ? 1 : 0;
						settype($value, $deftype);
					}

					break;

				case 'string':
					if ($newtype == 'integer' || $newtype == 'double')
					{
						$value = (string) $value;
					}
					elseif ($newtype  == 'boolean')
					{
						$value = $value ? '1' : '0';
					}

					break;
			}

			if (gettype($value) != $deftype)
			{
				CliLib::invalidType($value, $deftype);
			}
		}

		return $value;
	}

	public function getSetting($name)
	{
		try
		{
			return $this->resolveReference($name, array(), 'settings');
		}
		catch (CliReferenceException $ex)
		{
			$setting = $this->mode->getSetting($name);
			return $setting['default'];
		}
	}

	public function setUserfunc ($name, array $args, ExpressionStmt $exp)
	{
		// Make sure the identifier is not reserved
		if ($this->mode->getIdentifierType($index) !== false ||
			isset($this->variables[$index]))
		{
			throw new CliReservedException(
				"Cannot redefine identifier'$index' as a variable");
		}

		$this->userfuncs[$name] = array(
			'args' => $args,
			'func' => $exp,
		);
	}

	public function getUserfunc($name)
	{
		try
		{
			return $this->resolveReference($name, array(), 'functions');
		}
		catch (CliUndefinedReferenceException $ex)
		{
			throw new CliUndefinedFunctionException(
				"Undefined function '$name'");
		}
	}

	private function & resolveReference ($name, array $indexes, $type)
	{

		// Make sure we have such variable
		if (!isset($this->{$type}[$name]))
		{
			// If we don't check if our parent context has
			if ($this->parent !== false)
			{
				return $this->parent->resolveReference($name, $indexes, $type);
			}

			throw new CliUndefinedReferenceException(
				"Undefined reference '$name'");
		}

		$ref = & $this->{$type}[$name];

		foreach ($indexes as $index)
		{
			if (!is_array($ref) || !isset($ref[$index]))
			{
				throw new CliUndefinedArrayReferenceException(
					"Undefined array index '$index'");
			}

			$ref = & $ref[$index];
		}

		return $ref;
	}

	public function importData (array $data)
	{
		$this->errors = array();
		$this->parser = new CliParser($this->mode);

		if (isset($data['variables']))
		{
			$variables = $data['variables'];
			array_walk_recursive($variables, array($this, 'importVariable'));
			$this->variables = $variables;
		}
		if (isset($data['settings']))
		{
			$settings = $data['settings'];
			array_walk($settings, array($this, 'importSetting'));
			$this->settings = $settings;
		}
		if (isset($data['userfuncs']))
		{
			$userfuncs = $data['userfuncs'];
			array_walk($userfuncs, array($this, 'importUserfunc'));
			$this->userfuncs = $userfuncs;
		}

		$this->parser = null;
	}

	public function exportData ()
	{
		$variables = $this->variables;
		$settings = $this->settings;
		$userfuncs = $this->userfuncs;

		array_walk_recursive($variables, array($this, 'exportVariable'));
		array_walk($settings, array($this, 'exportSetting'));
		array_walk($userfuncs, array($this, 'exportUserfunc'));

		return array(
			'variables' => $variables,
			'settings' => $settings,
			'userfuncs' => $userfuncs,
		);
	}

	public function getImportErrors ()
	{
		return $this->errors;
	}

	/* ############### Data import & Export ############### */

	public function exportVariable (& $item)
	{
		// Ints, floats and booleans can be passed as is
		if (is_int($item) || is_float($item) || is_bool($item))
		{
			// No need for further action
		}
		// Strings must be preceded by the string key
		elseif (is_string($item))
		{
			$item = "string:$item";
		}
		// Expressions are saved in RPN notation
		elseif ($item instanceof ExpressionStmt)
		{
			$item = "expression:" . (string) $item;
		}
		// Variables are saved by the name (since it's only a reference)
		elseif ($item instanceof ExpVariable)
		{
			$item = "variable:" . (string) $item;
		}
		elseif ($item instanceof ExpModeNumber)
		{
			$item = "modenumber:" . (string) $item;
		}
		else
		{
			$type = gettype($item);
			throw new CliDataException("Unknown variable type '$type'");
		}

		return true;
	}

	public function exportSetting (& $item)
	{
		if (is_int($item) || is_float($item) || is_bool($item) || is_string($item))
		{
			// No need for further action
		}
		else
		{
			throw new CliDataException("Unknown setting type");
		}

		return true;
	}

	public function exportUserfunc (& $item)
	{
		if (!is_array($item) || array_keys($item) != array('args', 'func'))
		{
			throw new CliDataException("Invalid User Function format");
		}

		$item['func'] = (string) $item['func'];
	}

	public function importVariable (& $item)
	{
		if (is_int($item) || is_float($item) || is_bool($item))
		{
			// No need for further action
		}
		elseif (is_string($item))
		{
			if (strpos($item, ':') === false)
			{
				throw new CliDataException("Invalid string format '$item'");
			}

			list($type, $data) = explode(':', $item, 2);

			switch ($type)
			{
				case 'modenumber':
					try
					{
						$number = $this->parser->getNumber($data);

						if ($number === false)
						{
							throw new CliDataException(
								"Invalid modenumber format '$data'");
						}

						$item = $number;
					}
					catch (CliParserException $ex)
					{
						$item = null;
						$this->errors = $ex;
					}
					break;

				case 'string':
					$item = $data;
					break;

				case 'variable':
					try
					{
						$item = new ExpVariable($data , $this->mode);
					}
					catch (CliReferenceException $ex)
					{
						$item = null;
						$this->errors[] = $ex;
					}
					break;

				case 'expression':
					try
					{
						$item = $this->parser->parseExpression($data);
					}
					catch (CliParserException $ex)
					{
						$item = null;
						$this->errors[] = $ex;
					}
					break;

				default:
					throw new CliDataException("Unknown string type '$type'");
			}
		}
		else
		{
			$type = gettype($item);
			throw new CliDataException("Unknown variable type '$type'");
		}

		return true;
	}

	public function importSetting (& $item)
	{
		if (is_int($item) || is_float($item) || is_bool($item) || is_string($item))
		{
			// No need for further action
		}
		else
		{
			throw new CliDataException("Unknown setting type");
		}

		return true;
	}

	public function importUserfunc (& $item)
	{
		if (!is_array($item) || array_keys($item) != array('args', 'func'))
		{
			throw new CliDataException("Invalid User Function format");
		}

		try
		{
			$item['func'] = $this->parser->parseExpression($item['func']);
		}
		catch (CliParserException $ex)
		{
			$item['func'] = new ExpressionStmt(array(0));
			$this->errors[] = $ex;
		}

		return true;
	}
}

?>